/**
 * @license
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
(function(window) {
  'use strict';

  // Navigation parameters object format:
  //
  // Each object has a `view` property with a value from Gerrit.Nav.View. The
  // remaining properties depend on the value used for view.
  //
  //  - Gerrit.Nav.View.CHANGE:
  //    - `changeNum`, required, String: the numeric ID of the change.
  //    - `project`, optional, String: the project name.
  //    - `patchNum`, optional, Number: the patch for the right-hand-side of
  //        the diff.
  //    - `basePatchNum`, optional, Number: the patch for the left-hand-side
  //        of the diff. If `basePatchNum` is provided, then `patchNum` must
  //        also be provided.
  //    - `edit`, optional, Boolean: whether or not to load the file list with
  //        edit controls.
  //    - `messageHash`, optional, String: the hash of the change message to
  //        scroll to.
  //
  // - Gerrit.Nav.View.SEARCH:
  //    - `query`, optional, String: the literal search query. If provided,
  //        the string will be used as the query, and all other params will be
  //        ignored.
  //    - `owner`, optional, String: the owner name.
  //    - `project`, optional, String: the project name.
  //    - `branch`, optional, String: the branch name.
  //    - `topic`, optional, String: the topic name.
  //    - `hashtag`, optional, String: the hashtag name.
  //    - `statuses`, optional, Array<String>: the list of change statuses to
  //        search for. If more than one is provided, the search will OR them
  //        together.
  //    - `offset`, optional, Number: the offset for the query.
  //
  //  - Gerrit.Nav.View.DIFF:
  //    - `changeNum`, required, String: the numeric ID of the change.
  //    - `path`, required, String: the filepath of the diff.
  //    - `patchNum`, required, Number: the patch for the right-hand-side of
  //        the diff.
  //    - `basePatchNum`, optional, Number: the patch for the left-hand-side
  //        of the diff. If `basePatchNum` is provided, then `patchNum` must
  //        also be provided.
  //    - `lineNum`, optional, Number: the line number to be selected on load.
  //    - `leftSide`, optional, Boolean: if a `lineNum` is provided, a value
  //        of true selects the line from base of the patch range. False by
  //        default.
  //
  //  - Gerrit.Nav.View.GROUP:
  //    - `groupId`, required, String: the ID of the group.
  //    - `detail`, optional, String: the name of the group detail view.
  //      Takes any value from Gerrit.Nav.GroupDetailView.
  //
  //  - Gerrit.Nav.View.REPO:
  //    - `repoName`, required, String: the name of the repo
  //    - `detail`, optional, String: the name of the repo detail view.
  //      Takes any value from Gerrit.Nav.RepoDetailView.
  //
  //  - Gerrit.Nav.View.DASHBOARD
  //    - `repo`, optional, String.
  //    - `sections`, optional, Array of objects with `title` and `query`
  //      strings.
  //    - `user`, optional, String.
  //
  //  - Gerrit.Nav.View.ROOT:
  //    - no possible parameters.

  window.Gerrit = window.Gerrit || {};

  // Prevent redefinition.
  if (window.Gerrit.hasOwnProperty('Nav')) { return; }

  const uninitialized = () => {
    console.warn('Use of uninitialized routing');
  };

  const EDIT_PATCHNUM = 'edit';
  const PARENT_PATCHNUM = 'PARENT';

  const USER_PLACEHOLDER_PATTERN = /\$\{user\}/g;

  // NOTE: These queries are tested in Java. Any changes made to definitions
  // here require corresponding changes to:
  // javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
  const DEFAULT_SECTIONS = [
    {
      // Changes with unpublished draft comments. This section is omitted when
      // viewing other users, so we don't need to filter anything out.
      name: 'Has draft comments',
      query: 'has:draft',
      selfOnly: true,
      hideIfEmpty: true,
      suffixForDashboard: 'limit:10',
    },
    {
      // Changes that are assigned to the viewed user.
      name: 'Assigned reviews',
      query: 'assignee:${user} (-is:wip OR owner:self OR assignee:self) ' +
          'is:open -is:ignored',
      hideIfEmpty: true,
      suffixForDashboard: 'limit:25',
    },
    {
      // WIP open changes owned by viewing user. This section is omitted when
      // viewing other users, so we don't need to filter anything out.
      name: 'Work in progress',
      query: 'is:open owner:${user} is:wip',
      selfOnly: true,
      hideIfEmpty: true,
      suffixForDashboard: 'limit:25',
    },
    {
      // Non-WIP open changes owned by viewed user. Filter out changes ignored
      // by the viewing user.
      name: 'Outgoing reviews',
      query: 'is:open owner:${user} -is:wip -is:ignored',
      isOutgoing: true,
      suffixForDashboard: 'limit:25',
    },
    {
      // Non-WIP open changes not owned by the viewed user, that the viewed user
      // is associated with (as either a reviewer or the assignee). Changes
      // ignored by the viewing user are filtered out.
      name: 'Incoming reviews',
      query: 'is:open -owner:${user} -is:wip -is:ignored ' +
          '(reviewer:${user} OR assignee:${user})',
      suffixForDashboard: 'limit:25',
    },
    {
      // Open changes the viewed user is CCed on. Changes ignored by the viewing
      // user are filtered out.
      name: 'CCed on',
      query: 'is:open -is:ignored cc:${user}',
      suffixForDashboard: 'limit:10',
    },
    {
      name: 'Recently closed',
      // Closed changes where viewed user is owner, reviewer, or assignee.
      // Changes ignored by the viewing user are filtered out, and so are WIP
      // changes not owned by the viewing user (the one instance of
      // 'owner:self' is intentional and implements this logic).
      query: 'is:closed -is:ignored (-is:wip OR owner:self) ' +
          '(owner:${user} OR reviewer:${user} OR assignee:${user} ' +
          'OR cc:${user})',
      suffixForDashboard: '-age:4w limit:10',
    },
  ];

  window.Gerrit.Nav = {

    View: {
      ADMIN: 'admin',
      AGREEMENTS: 'agreements',
      CHANGE: 'change',
      DASHBOARD: 'dashboard',
      DIFF: 'diff',
      DOCUMENTATION_SEARCH: 'documentation-search',
      EDIT: 'edit',
      GROUP: 'group',
      PLUGIN_SCREEN: 'plugin-screen',
      REPO: 'repo',
      ROOT: 'root',
      SEARCH: 'search',
      SETTINGS: 'settings',
    },

    GroupDetailView: {
      MEMBERS: 'members',
      LOG: 'log',
    },

    RepoDetailView: {
      ACCESS: 'access',
      BRANCHES: 'branches',
      COMMANDS: 'commands',
      DASHBOARDS: 'dashboards',
      TAGS: 'tags',
    },

    WeblinkType: {
      CHANGE: 'change',
      FILE: 'file',
      PATCHSET: 'patchset',
    },

    /** @type {Function} */
    _navigate: uninitialized,

    /** @type {Function} */
    _generateUrl: uninitialized,

    /** @type {Function} */
    _generateWeblinks: uninitialized,

    /** @type {Function} */
    mapCommentlinks: uninitialized,

    /**
     * @param {number=} patchNum
     * @param {number|string=} basePatchNum
     */
    _checkPatchRange(patchNum, basePatchNum) {
      if (basePatchNum && !patchNum) {
        throw new Error('Cannot use base patch number without patch number.');
      }
    },

    /**
     * Setup router implementation.
     *
     * @param {function(!string)} navigate the router-abstracted equivalent of
     *     `window.location.href = ...`. Takes a string.
     * @param {function(!Object): string} generateUrl generates a URL given
     *     navigation parameters, detailed in the file header.
     * @param {function(!Object): string} generateWeblinks weblinks generator
     *     function takes single payload parameter with type property that
     *  determines which
     *     part of the UI is the consumer of the weblinks. type property can
     *     be one of file, change, or patchset.
     *     - For file type, payload will also contain string properties: repo,
     *         commit, file.
     *     - For patchset type, payload will also contain string properties:
     *         repo, commit.
     *     - For change type, payload will also contain string properties:
     *         repo, commit. If server provides weblinks, those will be passed
     *         as options.weblinks property on the main payload object.
     * @param {function(!Object): Object} mapCommentlinks provides an escape
     *     hatch to modify the commentlinks object, e.g. if it contains any
     *     relative URLs.
     */
    setup(navigate, generateUrl, generateWeblinks, mapCommentlinks) {
      this._navigate = navigate;
      this._generateUrl = generateUrl;
      this._generateWeblinks = generateWeblinks;
      this.mapCommentlinks = mapCommentlinks;
    },

    destroy() {
      this._navigate = uninitialized;
      this._generateUrl = uninitialized;
      this._generateWeblinks = uninitialized;
      this.mapCommentlinks = uninitialized;
    },

    /**
     * Generate a URL for the given route parameters.
     *
     * @param {Object} params
     * @return {string}
     */
    _getUrlFor(params) {
      return this._generateUrl(params);
    },

    getUrlForSearchQuery(query, opt_offset) {
      return this._getUrlFor({
        view: Gerrit.Nav.View.SEARCH,
        query,
        offset: opt_offset,
      });
    },

    /**
     * @param {!string} project The name of the project.
     * @param {boolean=} opt_openOnly When true, only search open changes in
     *     the project.
     * @param {string=} opt_host The host in which to search.
     * @return {string}
     */
    getUrlForProjectChanges(project, opt_openOnly, opt_host) {
      return this._getUrlFor({
        view: Gerrit.Nav.View.SEARCH,
        project,
        statuses: opt_openOnly ? ['open'] : [],
        host: opt_host,
      });
    },

    /**
     * @param {string} branch The name of the branch.
     * @param {string} project The name of the project.
     * @param {string=} opt_status The status to search.
     * @param {string=} opt_host The host in which to search.
     * @return {string}
     */
    getUrlForBranch(branch, project, opt_status, opt_host) {
      return this._getUrlFor({
        view: Gerrit.Nav.View.SEARCH,
        branch,
        project,
        statuses: opt_status ? [opt_status] : undefined,
        host: opt_host,
      });
    },

    /**
     * @param {string} topic The name of the topic.
     * @param {string=} opt_host The host in which to search.
     * @return {string}
     */
    getUrlForTopic(topic, opt_host) {
      return this._getUrlFor({
        view: Gerrit.Nav.View.SEARCH,
        topic,
        statuses: ['open', 'merged'],
        host: opt_host,
      });
    },

    /**
     * @param {string} hashtag The name of the hashtag.
     * @return {string}
     */
    getUrlForHashtag(hashtag) {
      return this._getUrlFor({
        view: Gerrit.Nav.View.SEARCH,
        hashtag,
        statuses: ['open', 'merged'],
      });
    },

    /**
     * Navigate to a search for changes with the given status.
     *
     * @param {string} status
     */
    navigateToStatusSearch(status) {
      this._navigate(this._getUrlFor({
        view: Gerrit.Nav.View.SEARCH,
        statuses: [status],
      }));
    },

    /**
     * Navigate to a search query
     *
     * @param {string} query
     * @param {number=} opt_offset
     */
    navigateToSearchQuery(query, opt_offset) {
      return this._navigate(this.getUrlForSearchQuery(query, opt_offset));
    },

    /**
     * Navigate to the user's dashboard
     */
    navigateToUserDashboard() {
      return this._navigate(this.getUrlForUserDashboard('self'));
    },

    /**
     * @param {!Object} change The change object.
     * @param {number=} opt_patchNum
     * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
     *     used for none.
     * @param {boolean=} opt_isEdit
     * @param {string=} opt_messageHash
     * @return {string}
     */
    getUrlForChange(change, opt_patchNum, opt_basePatchNum, opt_isEdit,
        opt_messageHash) {
      if (opt_basePatchNum === PARENT_PATCHNUM) {
        opt_basePatchNum = undefined;
      }

      this._checkPatchRange(opt_patchNum, opt_basePatchNum);
      return this._getUrlFor({
        view: Gerrit.Nav.View.CHANGE,
        changeNum: change._number,
        project: change.project,
        patchNum: opt_patchNum,
        basePatchNum: opt_basePatchNum,
        edit: opt_isEdit,
        host: change.internalHost || undefined,
        messageHash: opt_messageHash,
      });
    },

    /**
     * @param {number} changeNum
     * @param {string} project The name of the project.
     * @param {number=} opt_patchNum
     * @return {string}
     */
    getUrlForChangeById(changeNum, project, opt_patchNum) {
      return this._getUrlFor({
        view: Gerrit.Nav.View.CHANGE,
        changeNum,
        project,
        patchNum: opt_patchNum,
      });
    },

    /**
     * @param {!Object} change The change object.
     * @param {number=} opt_patchNum
     * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
     *     used for none.
     * @param {boolean=} opt_isEdit
     */
    navigateToChange(change, opt_patchNum, opt_basePatchNum, opt_isEdit) {
      this._navigate(this.getUrlForChange(change, opt_patchNum,
          opt_basePatchNum, opt_isEdit));
    },

    /**
     * @param {{ _number: number, project: string }} change The change object.
     * @param {string} path The file path.
     * @param {number=} opt_patchNum
     * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
     *     used for none.
     * @param {number|string=} opt_lineNum
     * @return {string}
     */
    getUrlForDiff(change, path, opt_patchNum, opt_basePatchNum, opt_lineNum) {
      return this.getUrlForDiffById(change._number, change.project, path,
          opt_patchNum, opt_basePatchNum, opt_lineNum);
    },

    /**
     * @param {number} changeNum
     * @param {string} project The name of the project.
     * @param {string} path The file path.
     * @param {number=} opt_patchNum
     * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
     *     used for none.
     * @param {number=} opt_lineNum
     * @param {boolean=} opt_leftSide
     * @return {string}
     */
    getUrlForDiffById(changeNum, project, path, opt_patchNum,
        opt_basePatchNum, opt_lineNum, opt_leftSide) {
      if (opt_basePatchNum === PARENT_PATCHNUM) {
        opt_basePatchNum = undefined;
      }

      this._checkPatchRange(opt_patchNum, opt_basePatchNum);
      return this._getUrlFor({
        view: Gerrit.Nav.View.DIFF,
        changeNum,
        project,
        path,
        patchNum: opt_patchNum,
        basePatchNum: opt_basePatchNum,
        lineNum: opt_lineNum,
        leftSide: opt_leftSide,
      });
    },

    /**
     * @param {{ _number: number, project: string }} change The change object.
     * @param {string} path The file path.
     * @param {number=} opt_patchNum
     * @return {string}
     */
    getEditUrlForDiff(change, path, opt_patchNum) {
      return this.getEditUrlForDiffById(change._number, change.project, path,
          opt_patchNum);
    },

    /**
     * @param {number} changeNum
     * @param {string} project The name of the project.
     * @param {string} path The file path.
     * @param {number|string=} opt_patchNum The patchNum the file content
     *    should be based on, or ${EDIT_PATCHNUM} if left undefined.
     * @return {string}
     */
    getEditUrlForDiffById(changeNum, project, path, opt_patchNum) {
      return this._getUrlFor({
        view: Gerrit.Nav.View.EDIT,
        changeNum,
        project,
        path,
        patchNum: opt_patchNum || EDIT_PATCHNUM,
      });
    },

    /**
     * @param {!Object} change The change object.
     * @param {string} path The file path.
     * @param {number=} opt_patchNum
     * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
     *     used for none.
     */
    navigateToDiff(change, path, opt_patchNum, opt_basePatchNum) {
      this._navigate(this.getUrlForDiff(change, path, opt_patchNum,
          opt_basePatchNum));
    },

    /**
     * @param {string} owner The name of the owner.
     * @return {string}
     */
    getUrlForOwner(owner) {
      return this._getUrlFor({
        view: Gerrit.Nav.View.SEARCH,
        owner,
      });
    },

    /**
     * @param {string} user The name of the user.
     * @return {string}
     */
    getUrlForUserDashboard(user) {
      return this._getUrlFor({
        view: Gerrit.Nav.View.DASHBOARD,
        user,
      });
    },

    /**
     * @return {string}
     */
    getUrlForRoot() {
      return this._getUrlFor({
        view: Gerrit.Nav.View.ROOT,
      });
    },

    /**
     * @param {string} repo The name of the repo.
     * @param {string} dashboard The ID of the dashboard, in the form of
     *     '<ref>:<path>'.
     * @return {string}
     */
    getUrlForRepoDashboard(repo, dashboard) {
      return this._getUrlFor({
        view: Gerrit.Nav.View.DASHBOARD,
        repo,
        dashboard,
      });
    },

    /**
     * Navigate to an arbitrary relative URL.
     *
     * @param {string} relativeUrl
     */
    navigateToRelativeUrl(relativeUrl) {
      if (!relativeUrl.startsWith('/')) {
        throw new Error('navigateToRelativeUrl with non-relative URL');
      }
      this._navigate(relativeUrl);
    },

    /**
     * @param {string} repoName
     * @return {string}
     */
    getUrlForRepo(repoName) {
      return this._getUrlFor({
        view: Gerrit.Nav.View.REPO,
        repoName,
      });
    },

    /**
     * Navigate to a repo settings page.
     *
     * @param {string} repoName
     */
    navigateToRepo(repoName) {
      this._navigate(this.getUrlForRepo(repoName));
    },

    /**
     * @param {string} repoName
     * @return {string}
     */
    getUrlForRepoTags(repoName) {
      return this._getUrlFor({
        view: Gerrit.Nav.View.REPO,
        repoName,
        detail: Gerrit.Nav.RepoDetailView.TAGS,
      });
    },

    /**
     * @param {string} repoName
     * @return {string}
     */
    getUrlForRepoBranches(repoName) {
      return this._getUrlFor({
        view: Gerrit.Nav.View.REPO,
        repoName,
        detail: Gerrit.Nav.RepoDetailView.BRANCHES,
      });
    },

    /**
     * @param {string} repoName
     * @return {string}
     */
    getUrlForRepoAccess(repoName) {
      return this._getUrlFor({
        view: Gerrit.Nav.View.REPO,
        repoName,
        detail: Gerrit.Nav.RepoDetailView.ACCESS,
      });
    },

    /**
     * @param {string} repoName
     * @return {string}
     */
    getUrlForRepoCommands(repoName) {
      return this._getUrlFor({
        view: Gerrit.Nav.View.REPO,
        repoName,
        detail: Gerrit.Nav.RepoDetailView.COMMANDS,
      });
    },

    /**
     * @param {string} repoName
     * @return {string}
     */
    getUrlForRepoDashboards(repoName) {
      return this._getUrlFor({
        view: Gerrit.Nav.View.REPO,
        repoName,
        detail: Gerrit.Nav.RepoDetailView.DASHBOARDS,
      });
    },

    /**
     * @param {string} groupId
     * @return {string}
     */
    getUrlForGroup(groupId) {
      return this._getUrlFor({
        view: Gerrit.Nav.View.GROUP,
        groupId,
      });
    },

    /**
     * @param {string} groupId
     * @return {string}
     */
    getUrlForGroupLog(groupId) {
      return this._getUrlFor({
        view: Gerrit.Nav.View.GROUP,
        groupId,
        detail: Gerrit.Nav.GroupDetailView.LOG,
      });
    },

    /**
     * @param {string} groupId
     * @return {string}
     */
    getUrlForGroupMembers(groupId) {
      return this._getUrlFor({
        view: Gerrit.Nav.View.GROUP,
        groupId,
        detail: Gerrit.Nav.GroupDetailView.MEMBERS,
      });
    },

    getUrlForSettings() {
      return this._getUrlFor({view: Gerrit.Nav.View.SETTINGS});
    },

    /**
     * @param {string} repo
     * @param {string} commit
     * @param {string} file
     * @param {Object=} opt_options
     * @return {
     *   Array<{label: string, url: string}>|
     *   {label: string, url: string}
     *  }
     */
    getFileWebLinks(repo, commit, file, opt_options) {
      const params = {type: Gerrit.Nav.WeblinkType.FILE, repo, commit, file};
      if (opt_options) {
        params.options = opt_options;
      }
      return [].concat(this._generateWeblinks(params));
    },

    /**
     * @param {string} repo
     * @param {string} commit
     * @param {Object=} opt_options
     * @return {{label: string, url: string}}
     */
    getPatchSetWeblink(repo, commit, opt_options) {
      const params = {type: Gerrit.Nav.WeblinkType.PATCHSET, repo, commit};
      if (opt_options) {
        params.options = opt_options;
      }
      const result = this._generateWeblinks(params);
      if (Array.isArray(result)) {
        return result.pop();
      } else {
        return result;
      }
    },

    /**
     * @param {string} repo
     * @param {string} commit
     * @param {Object=} opt_options
     * @return {
     *   Array<{label: string, url: string}>|
     *   {label: string, url: string}
     *  }
     */
    getChangeWeblinks(repo, commit, opt_options) {
      const params = {type: Gerrit.Nav.WeblinkType.CHANGE, repo, commit};
      if (opt_options) {
        params.options = opt_options;
      }
      return [].concat(this._generateWeblinks(params));
    },

    getUserDashboard(user = 'self', sections = DEFAULT_SECTIONS,
        title = '') {
      sections = sections
          .filter(section => (user === 'self' || !section.selfOnly))
          .map(section => Object.assign({}, section, {
            name: section.name,
            query: section.query.replace(USER_PLACEHOLDER_PATTERN, user),
          }));
      return {title, sections};
    },
  };
})(window);
